package com.shisan.tingshu.common.login.aspect;

import com.alibaba.fastjson.JSONObject;
import com.shisan.tingshu.common.constant.PublicConstant;
import com.shisan.tingshu.common.constant.RedisConstant;
import com.shisan.tingshu.common.execption.ShisanException;
import com.shisan.tingshu.common.login.annotation.TingshuLogin;
import com.shisan.tingshu.common.result.ResultCodeEnum;
import com.shisan.tingshu.common.util.AuthContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.util.Map;

@Aspect
@Component
public class LoginAspect {


    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 定义切面逻辑。判断用户是否登录，未登录则跳转登录页面
     */
    @Around(value = "@annotation(com.shisan.tingshu.common.login.annotation.TingshuLogin)")
    // 此处为什么选择抛出异常，而不是try-catch捕捉异常？
    // 因为在多切面情况下，比如有登录切面和事务切面
    // 如果使用try-catch捕捉异常，那么事务切面将无法捕捉到异常，会导致事务无法回滚，出现多切面的失效问题
    public Object loginCheck(ProceedingJoinPoint pjp) throws Throwable {

        // 获取请求中的token
        String jsonWebToken = getJsonWebToken();

        // 获取目标方法的注解及属性值
        Boolean isLogin = getMethodLoginAnnotationValue(pjp);
        if(!isLogin) {
            // 如果没登录，则直接放行。有登录则跳过下面的if，执行下面的逻辑
            if (StringUtils.isEmpty(jsonWebToken)) {
                return pjp.proceed();
            }
        }

        // 判断获取的token是否为空，若不为空则获取到其中的载荷数据
        Long userId = checkTokenAndGetUserId(jsonWebToken);
        // 因为执行切面逻辑的线程和执行目标方法的线程是同一个，所以可以将userId放入ThreadLocal中
        AuthContextHolder.setUserId(userId); // AuthContextHolder是自定义的获取当前用户信息帮助类

        // 执行目标方法
        Object retCal;
        try {
            retCal = pjp.proceed();
        } finally {
            // ThreadLocal 在使用的过程中注意内存泄漏的问题
            // 内存泄漏：一个线程将对象（数据）存储到了内存的堆空间，当这个线程执行完任务后，却没有将对象（数据）从堆空间中移除
            // 内存泄漏的次数多了，就会导致OOM
            AuthContextHolder.removeUserId();
            // 当然：使用 ThreadLocal 也不一定会造成内存泄漏。比如：
            // ThreadLocal threadLocal = new ThreadLocal();
            // threadLocal.set("1");
            // 因为这个 1 被放在 ThreadLocalMap 中，这个线程执行完任务后，这个 ThreadLocalMap 会连着 ThreadLocal 一起被销毁，所以不会造成内存泄漏
            // 但是：使用线程池时，极易造成内存泄漏。因为线程池中的线程是复用的，当线程执行完任务后，不会被销毁，而是继续执行下一个任务
            // 所以上面是属于第二种情况，此处的线程是Tomcat线程池（默认有10个线程，最多200个）分配的
        }

        // 返回通知
        return retCal;
    }

    /**
     * 获取目标方法的注解及属性值
     * @param pjp
     * @return
     */
    private Boolean getMethodLoginAnnotationValue(ProceedingJoinPoint pjp) {

        // 获取方法签名对象
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        // 获取方法对象
        Method method = methodSignature.getMethod();
        // 获取方法上的TingshuLogin注解对象
        TingshuLogin annotation = method.getAnnotation(TingshuLogin.class);
        // 获取注解对象的属性值
        boolean required = annotation.required();

        return required;
    }

    private Long checkTokenAndGetUserId(String jsonWebTokenFromWeb) {
        // 判断token是否为空
        if(StringUtils.isEmpty(jsonWebTokenFromWeb)) {
            throw new ShisanException(ResultCodeEnum.LOGIN_AUTH);
        }
        // 校验jsonWebToken是否有被篡改。传入token和公钥（验签）。若被篡改会自动抛出异常，没有则往下继续执行
        Jwt jwt = JwtHelper.decodeAndVerify(jsonWebTokenFromWeb, new RsaVerifier(PublicConstant.PUBLIC_KEY));
        // 校验通过，获取载荷数据
        String claims = jwt.getClaims();
        Map map = JSONObject.parseObject(claims, Map.class);
        Object userId = map.get("id");
        Object openid = map.get("openId");
        // 判断Redis中的accessToken是否存在，同时将jsonWebTokenFromWeb与Redis中的accessToken进行对比
        String accessTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + openid; // k
        String accessTokenKeyFromRedis = redisTemplate.opsForValue().get(accessTokenKey); // 通过k得到v
        if(StringUtils.isEmpty(accessTokenKeyFromRedis)) {
            throw new ShisanException(401, "token 不存在");
        }
        if(!jsonWebTokenFromWeb.equals(accessTokenKeyFromRedis)) {
            throw new ShisanException(401, "token 已过期");
        }

        return Long.parseLong(userId.toString());
    }

    /**
     * 获取请求头中的token
     */
    private static String getJsonWebToken() {

        // 获取目标请求属性对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        // 获取请求对象
        HttpServletRequest request = requestAttributes.getRequest();
        // 获取请求头
        String token = request.getHeader("token");

        return token;
    }
}
